﻿//-----------------------------------------------------------------------
// <copyright file="EntityFrameworkRepository.cs" company="YuGuan Corporation">
//     Copyright (c) YuGuan Corporation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace DevLib.Repository.EntityFramework
{
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;
    using System.Data.Entity.Migrations;
    using System.Linq;
    using System.Linq.Expressions;

    /// <summary>
    /// Entity Framework Repository.
    /// </summary>
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
    public class EntityFrameworkRepository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        /// <summary>
        /// The database context.
        /// </summary>
        private readonly DbContext _dbContext;

        /// <summary>
        /// The table.
        /// </summary>
        private readonly DbSet<TEntity> _table;

        /// <summary>
        /// The table name.
        /// </summary>
        private readonly string _tableName;

        /// <summary>
        /// Field _disposed.
        /// </summary>
        private bool _disposed = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityFrameworkRepository{TEntity}"/> class.
        /// </summary>
        public EntityFrameworkRepository()
        {
            this.ThrowOnError = true;
            this._dbContext = new EntityFrameworkRepositoryDbContext<TEntity>();
            this._table = this._dbContext.Set<TEntity>();

            try
            {
                this._tableName = this._dbContext.GetTableName<TEntity>();
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityFrameworkRepository{TEntity}"/> class.
        /// </summary>
        /// <param name="nameOrConnectionString">Either the database name or a connection string.</param>
        public EntityFrameworkRepository(string nameOrConnectionString)
        {
            this.ThrowOnError = true;
            this._dbContext = new EntityFrameworkRepositoryDbContext<TEntity>(nameOrConnectionString);
            this._table = this._dbContext.Set<TEntity>();

            try
            {
                this._tableName = this._dbContext.GetTableName<TEntity>();
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityFrameworkRepository{TEntity}"/> class.
        /// </summary>
        /// <param name="dbContext">The database context.</param>
        public EntityFrameworkRepository(DbContext dbContext)
        {
            this.ThrowOnError = true;
            this._dbContext = dbContext;
            this._table = this._dbContext.Set<TEntity>();

            try
            {
                this._tableName = this._dbContext.GetTableName<TEntity>();
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);
            }
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="EntityFrameworkRepository{TEntity}"/> class.
        /// </summary>
        ~EntityFrameworkRepository()
        {
            this.Dispose(false);
        }

        /// <summary>
        /// Gets or sets a value indicating whether throw exception on any error.
        /// </summary>
        public bool ThrowOnError
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets this property to log the SQL generated by the System.Data.Entity.DbContext to the given delegate.
        /// For example, to log to the console, set this property to System.Console.Write(System.String).
        /// </summary>
        public Action<string> Log
        {
            get
            {
                return this._dbContext.Database.Log;
            }

            set
            {
                this._dbContext.Database.Log = value;
            }
        }

        /// <summary>
        /// Gets or sets the timeout value, in seconds, for all context operations.
        /// The default value is null, where null indicates that the default value of the underlying provider will be used.
        /// </summary>
        public int? Timeout
        {
            get
            {
                return this._dbContext.Database.CommandTimeout;
            }

            set
            {
                this._dbContext.Database.CommandTimeout = value;
            }
        }

        /// <summary>
        /// Gets the underlying database.
        /// </summary>
        public Database InnerDatabase
        {
            get
            {
                return this._dbContext.Database;
            }
        }

        /// <summary>
        /// Gets the underlying table.
        /// </summary>
        public DbSet<TEntity> InnerTable
        {
            get
            {
                return this._table;
            }
        }

        /// <summary>
        /// Gets the underlying DbContext.
        /// </summary>
        public DbContext InnerDbContext
        {
            get
            {
                return this._dbContext;
            }
        }

        /// <summary>
        /// Gets the underlying table name.
        /// </summary>
        public string InnerTableName
        {
            get
            {
                return this._tableName;
            }
        }

        /// <summary>
        /// Creates new entity.
        /// </summary>
        /// <returns>TEntity instance.</returns>
        public TEntity Create()
        {
            this.CheckDisposed();

            try
            {
                return this._table.Create();
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Inserts the specified entity.
        /// </summary>
        /// <param name="entity">The entity to insert.</param>
        /// <returns>Inserted TEntity instance.</returns>
        public TEntity Insert(TEntity entity)
        {
            this.CheckDisposed();

            try
            {
                var result = this._table.Add(entity);
                this._dbContext.SaveChanges();
                return result;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entity);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Inserts the given collection of entities.
        /// </summary>
        /// <param name="entities">The collection of entities to insert.</param>
        /// <returns>The collection of entities.</returns>
        public IEnumerable<TEntity> Insert(IEnumerable<TEntity> entities)
        {
            this.CheckDisposed();

            try
            {
                var result = this._table.AddRange(entities);
                this._dbContext.SaveChanges();
                return result;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entities);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Inserts the specified entity, if already exist then update.
        /// </summary>
        /// <param name="entity">The entity to insert or update.</param>
        /// <returns>Inserted TEntity instance.</returns>
        public TEntity InsertOrUpdate(TEntity entity)
        {
            this.CheckDisposed();

            try
            {
                this._table.AddOrUpdate(entity);
                this._dbContext.SaveChanges();
                return entity;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entity);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Inserts the given collection of entities, if already exist then update.
        /// </summary>
        /// <param name="entities">The collection of entities to insert or update.</param>
        /// <returns>The collection of entities.</returns>
        public IEnumerable<TEntity> InsertOrUpdate(IEnumerable<TEntity> entities)
        {
            this.CheckDisposed();

            try
            {
                this._table.AddOrUpdate(entities.ToArray());
                this._dbContext.SaveChanges();
                return entities;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entities);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Deletes an entity with the given primary key values.
        /// </summary>
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
        /// <returns>The entity found, or null.</returns>
        public TEntity Delete(params object[] keyValues)
        {
            this.CheckDisposed();

            TEntity result = default(TEntity);

            try
            {
                result = this._table.Find(keyValues);

                if (result != null)
                {
                    result = this._table.Remove(result);
                    this._dbContext.SaveChanges();
                }

                return result;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(result);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Deletes the specified entity.
        /// </summary>
        /// <param name="entity">The entity to delete.</param>
        /// <returns>The entity found, or null.</returns>
        public TEntity Delete(TEntity entity)
        {
            this.CheckDisposed();

            try
            {
                this._dbContext.Entry(entity).State = EntityState.Deleted;
                this._dbContext.SaveChanges();
                return entity;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entity);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Deletes the given collection of entities.
        /// </summary>
        /// <param name="entities">The collection of entities to delete.</param>
        /// <returns>The collection of entities.</returns>
        public IEnumerable<TEntity> Delete(IEnumerable<TEntity> entities)
        {
            this.CheckDisposed();

            try
            {
                foreach (var entity in entities)
                {
                    this._dbContext.Entry(entity).State = EntityState.Deleted;
                }

                this._dbContext.SaveChanges();

                return entities;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entities);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Deletes the given collection of entities.
        /// </summary>
        /// <param name="predicate">A function to test entity for a condition.</param>
        /// <returns>The collection of entities.</returns>
        public IEnumerable<TEntity> Delete(Expression<Func<TEntity, bool>> predicate)
        {
            this.CheckDisposed();

            IEnumerable<TEntity> result = null;

            try
            {
                result = this.Select(predicate);

                foreach (var entity in result)
                {
                    this._dbContext.Entry(entity).State = EntityState.Deleted;
                }

                this._dbContext.SaveChanges();

                return result;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(result);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Deletes all entities.
        /// </summary>
        /// <returns>The number of entries deleted.</returns>
        public int DeleteAll()
        {
            this.CheckDisposed();

            try
            {
                var objectContext = ((IObjectContextAdapter)this._dbContext).ObjectContext;
                return objectContext.ExecuteStoreCommand("TRUNCATE TABLE " + this._tableName);
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                try
                {
                    this._table.RemoveRange(this._table);
                    return this._dbContext.SaveChanges();
                }
                catch (Exception ex)
                {
                    InternalLogger.Log(ex);

                    if (this.ThrowOnError)
                    {
                        throw;
                    }

                    return 0;
                }
            }
        }

        /// <summary>
        /// Updates the specified entity.
        /// </summary>
        /// <param name="entity">The entity to update.</param>
        /// <returns>The entity found, or null.</returns>
        public TEntity Update(TEntity entity)
        {
            this.CheckDisposed();

            try
            {
                this._dbContext.Entry(entity).State = EntityState.Modified;
                this._dbContext.SaveChanges();
                return entity;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entity);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Updates the specified entity with specified properties.
        /// </summary>
        /// <param name="entity">The entity to update.</param>
        /// <param name="properties">The properties to update.</param>
        /// <returns>true if succeeded; otherwise, false.</returns>
        public bool Update(TEntity entity, params Expression<Func<TEntity, object>>[] properties)
        {
            this.CheckDisposed();

            try
            {
                this._table.Attach(entity);
                var entry = this._dbContext.Entry(entity);

                foreach (var property in properties)
                {
                    entry.Property(property).IsModified = true;
                }

                return this._dbContext.SaveChanges() > 0;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entity);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return false;
            }
        }

        /// <summary>
        /// Updates the given collection of entities.
        /// </summary>
        /// <param name="entities">The collection of entities to update.</param>
        /// <returns>The collection of entities.</returns>
        public IEnumerable<TEntity> Update(IEnumerable<TEntity> entities)
        {
            this.CheckDisposed();

            try
            {
                foreach (var entity in entities)
                {
                    this._dbContext.Entry(entity).State = EntityState.Modified;
                }

                this._dbContext.SaveChanges();

                return entities;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                this.Detach(entities);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Checks whether the specified entity exists or not.
        /// </summary>
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
        /// <returns>true if exists; otherwise, false.</returns>
        public bool Exists(params object[] keyValues)
        {
            this.CheckDisposed();

            try
            {
                return this._table.Find(keyValues) != null;
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return false;
            }
        }

        /// <summary>
        /// Checks whether the specified entity exists or not.
        /// </summary>
        /// <param name="predicate">A function to test entity for a condition.</param>
        /// <returns>true if exists; otherwise, false.</returns>
        public bool Exists(Expression<Func<TEntity, bool>> predicate)
        {
            this.CheckDisposed();

            try
            {
                return this._table.AsNoTracking().Any(predicate);
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return false;
            }
        }

        /// <summary>
        /// Finds an entity with the given primary key values.
        /// </summary>
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
        /// <returns>The entity found, or null.</returns>
        public TEntity Select(params object[] keyValues)
        {
            this.CheckDisposed();

            try
            {
                return this._table.Find(keyValues);
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Selects the entities based on a predicate.
        /// </summary>
        /// <param name="predicate">A function to test entity for a condition.</param>
        /// <returns>The collection of entities.</returns>
        public IEnumerable<TEntity> Select(Expression<Func<TEntity, bool>> predicate)
        {
            this.CheckDisposed();

            try
            {
                return this._table.AsNoTracking().Where(predicate).AsEnumerable();
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Selects all entities.
        /// </summary>
        /// <returns>The collection of entities as IQueryable{T}.</returns>
        public IQueryable<TEntity> SelectAll()
        {
            this.CheckDisposed();

            try
            {
                return this._table.AsNoTracking().AsQueryable();
            }
            catch (Exception e)
            {
                InternalLogger.Log(e);

                if (this.ThrowOnError)
                {
                    throw;
                }

                return null;
            }
        }

        /// <summary>
        /// Detaches the specified entity.
        /// </summary>
        /// <param name="entity">The entity.</param>
        public void Detach(TEntity entity)
        {
            if (entity != null)
            {
                try
                {
                    this._dbContext.Entry(entity).State = EntityState.Detached;
                }
                catch (Exception e)
                {
                    InternalLogger.Log(e);
                }
            }
        }

        /// <summary>
        /// Detaches the specified entities.
        /// </summary>
        /// <param name="entities">The entities.</param>
        public void Detach(IEnumerable<TEntity> entities)
        {
            if (entities != null)
            {
                try
                {
                    foreach (var entity in entities)
                    {
                        this.Detach(entity);
                    }
                }
                catch (Exception e)
                {
                    InternalLogger.Log(e);
                }
            }
        }

        /// <summary>
        /// Releases all resources used by the current instance of the <see cref="Repository" /> class.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases all resources used by the current instance of the <see cref="Repository" /> class.
        /// protected virtual for non-sealed class; private for sealed class.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (this._disposed)
            {
                return;
            }

            this._disposed = true;

            if (disposing)
            {
                // dispose managed resources
                ////if (managedResource != null)
                ////{
                ////    managedResource.Dispose();
                ////    managedResource = null;
                ////}

                if (this._dbContext != null)
                {
                    this._dbContext.Dispose();
                }
            }

            // free native resources
            ////if (nativeResource != IntPtr.Zero)
            ////{
            ////    Marshal.FreeHGlobal(nativeResource);
            ////    nativeResource = IntPtr.Zero;
            ////}
        }

        /// <summary>
        /// Checks whether this instance is disposed.
        /// </summary>
        private void CheckDisposed()
        {
            if (this._disposed)
            {
                throw new ObjectDisposedException($"DevLib.Repository.EntityFramework.EntityFrameworkRepository<{typeof(TEntity).Name}>");
            }
        }
    }
}
